<?php

/**
 * This file is part of the Codizer package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @copyright   Copyright (c) 2010 Advisa (http://www.advisa.fr)
 * @author      Codizer Core Team <team@codizer.org>
 * @package     Codizer
 */

class f_document_QueryBuilder
{
	private $query;
	private $conditions;
	private $orderConditions;
	private $publicationDatesCondition;
	private $values;
	private $fieldsToGet;
	private $columnsToGet;

	private $fieldsToGetString;
	private $columnsToGetString;
	private $joinTableFieldsToGetString;

	private $isLocalized;

	private $tableName;
	private $i18nTableName;
	private $className;

	private $nonLocalizedFields;
	private $localizedFields;
	private $groupFields;
	private $limitNbResult;
	private $limitOffset;

    private $distinct = false;
    private $relationQuery = array();


	/**
	 * @return f_document_QueryBuilder
	 */
	private function __construct()
	{
		return $this;
	}

	/**
	 * Get singleton instance
	 *
	 * @return f_document_QueryBuilder
	 */
	public static function getInstance()
	{
		return new self();
	}

	/**
	 * Init a new searching query by document model
	 *
	 * @return f_document_QueryBuilder
	 */
	public function initByModel($modelName)
	{
		$docProvider = f_document_Provider::getInstance();

		$this->tableName = $docProvider->resolveTableName($modelName);
		$this->className = $docProvider->resolveClassName($modelName);

		// init an empty object to get localization datas
		$obj = new $this->className();

		$this->isLocalized = $obj->isLocalized();

		if($this->isLocalized)
		{
			$this->i18nTableName = $this->tableName."_i18n";
			$fields = $this->getNonLocalizedFields($obj);
			$this->nonLocalizedFields = $fields['nonLocalized'];
			$this->localizedFields = $fields['localizedFields'];
		}
		return $this;
	}

// ================================= Conditions methods

	/**
	 * Add a new = condition in SELECT query
	 * To add an OR condition just pass an array to $value
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addEqualCondition($fieldName, $value, $andOr = 'AND')
	{
		if(!is_array($value))
		{
            $varName = $fieldName.count($this->conditions[$andOr]);
			$this->conditions[$andOr][] = "`$fieldName` = :eq".$varName;
			$this->values['eq'.$varName] = $value;
			$this->fieldsToGet[] = $fieldName;
		}
		else
		{
            $varName = $fieldName.count($this->conditions[$andOr]);
			$conditions = array();
			foreach($value as $key => $val)
			{
				$conditions[] = ':eq'.$varName.$key;
				$this->values['eq'.$varName.$key] = $val;
			}

            $this->conditions[$andOr][] = "`$fieldName` IN(".join(',',$conditions).")";
            $this->fieldsToGet[] = $fieldName;
		}
	}

	/**
	 * Add a new != condition in SELECT query
	 * To add an OR condition just pass an array to $value
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addNotEqualCondition($fieldName, $value, $andOr = 'AND')
	{
		if(!is_array($value))
		{
            $varName = $fieldName.count($this->conditions[$andOr]);
			$this->conditions[$andOr][] = "`$fieldName` != :ne".$varName;
			$this->values['ne'.$varName] = $value;
			$this->fieldsToGet[] = $fieldName;
		}
		else
		{
            $varName = $fieldName.count($this->conditions[$andOr]);
			$conditions = array();
			foreach($value as $key => $val)
			{
				$conditions[] = ':ne'.$varName.$key;
				$this->values['ne'.$varName.$key] = $val;
			}

            $this->conditions[$andOr][] = "`$fieldName` NOT IN(".join(',',$conditions).")";
            $this->fieldsToGet[] = $fieldName;
		}
	}

	/**
	 * Add a new < condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addLessThanCondition($fieldName, $value, $andOr = 'AND')
	{
		$this->conditions[$andOr][] = "`$fieldName` < :lt".$fieldName;
		$this->values['lt'.$fieldName] = $value;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new > condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addMoreThanCondition($fieldName, $value, $andOr = 'AND')
	{
		$this->conditions[$andOr][] = "`$fieldName` > :mt".$fieldName;
		$this->values['mt'.$fieldName] = $value;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new <= condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addLessThanOrEqualCondition($fieldName, $value, $andOr = 'AND')
	{
		$this->conditions[$andOr][] = "`$fieldName` <= :le".$fieldName;
		$this->values['le'.$fieldName] = $value;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new >= condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addMoreThanOrEqualCondition($fieldName, $value, $andOr = 'AND')
	{
		$this->conditions[$andOr][] = "`$fieldName` >= :me".$fieldName;
		$this->values['me'.$fieldName] = $value;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new LIKE condition in SELECT query
	 * To add an OR condition just pass an array to $value
	 *
	 * @param string $fieldName
	 * @param mixed $value
	 */
	public function addLikeCondition($fieldName, $value, $andOr = 'AND')
	{
		if(!is_array($value))
		{
			$this->conditions[$andOr][] = "`$fieldName` LIKE :li".$fieldName;
			$this->values['li'.$fieldName] = $value;
			$this->fieldsToGet[] = $fieldName;
		}
		else
		{
			$conditions = array();
			foreach($value as $key => $val)
			{
				$conditions[] =  "`$fieldName` LIKE :li".$fieldName.$key;
				$this->values['li'.$fieldName.$key] = $val;
			}

			$this->conditions[$andOr][] = "(".join(" OR ", $conditions).")";
			$this->fieldsToGet[] = $fieldName;
		}
	}

	/**
	 * Check if current date is between publication start and end
	 */
	public function addPublicationDatesCheck()
	{
		if($this->isLocalized)
		{
			$this->publicationDatesCondition = '(joinTable.`document_startpublicationdate` <= NOW() OR joinTable.`document_startpublicationdate` IS NULL) AND (joinTable.`document_endpublicationdate` >= NOW() OR joinTable.`document_endpublicationdate` IS NULL)';
		}
		else
		{
			$this->publicationDatesCondition = '(`document_startpublicationdate` <= NOW() OR `document_startpublicationdate` IS NULL) AND (`document_endpublicationdate` >= NOW() OR `document_endpublicationdate` IS NULL)';
		}

		$this->fieldsToGet[] = "document_startpublicationdate";
		$this->fieldsToGet[] = "document_endpublicationdate";
	}

	/**
	 * Check where start publication date is after current date
	 */
	public function addCheckToCome($fieldName, $andOr = 'AND')
	{
		$this->conditions[$andOr][] = "`$fieldName` > NOW()";
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Check where end publication date is before current date
	 */
	public function addCheckPast($fieldName, $andOr = 'AND')
	{
		$this->conditions[$andOr][] = "`$fieldName` < NOW()";
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new BETWEEN condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param date $dateBegin
	 * @param date $dateEnd
	 */
	public function addDateBetweenCondition($fieldName, $dateBegin, $dateEnd, $andOr = 'AND')
	{
		$counter = count($this->conditions[$andOr]);
		$this->conditions[$andOr][] = "`$fieldName` BETWEEN :dateBegin".$counter." AND :dateEnd".$counter;
		$this->values['dateBegin'.$counter] = $dateBegin;
		$this->values['dateEnd'.$counter] = $dateEnd;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new > date condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param date $dateBegin
	 */
	public function addDateAfterCondition($fieldName, $dateBegin, $andOr = 'AND')
	{
		$counter = count($this->conditions[$andOr]);
		$this->conditions[$andOr][] = "`$fieldName` >= :dateBegin".$counter;
		$this->values['dateBegin'.$counter] = $dateBegin;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add a new < date condition in SELECT query
	 *
	 * @param string $fieldName
	 * @param date $dateBegin
	 */
	public function addDateBeforeCondition($fieldName, $dateEnd, $andOr = 'AND')
	{
		$counter = count($this->conditions);
		$this->conditions[$andOr][] = "`$fieldName` <= :dateEnd".$counter;
		$this->values['dateEnd'.$counter] = $dateEnd;
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add child related document id query in SELECT query
	 *
	 * @param integer $parentId
	 * @param string $type type of relation (relation name)
     * @param boolean $addRelationFields set to true if you want to request on f_relation fields
	 */
	public function addChildOfRelationCondition($parentId, $type = null, $addRelationFields = false, $andOr = 'AND')
	{
		$lang = f_core_Context::getInstance()->getLang();

		if(!is_null($type))
		{
			$type = " AND `relation_type`='$type'";
		}

		$this->fieldsToGet[] = "document_id";

        $this->conditions[$andOr][] = "`parent_id`=".$parentId." AND `lang`='".$lang."'".$type;
        $this->relationQuery[] = "child_id";
        $this->addDistinct();
	}

	/**
	 * Add parent related document id query in SELECT query
	 *
	 * @param integer $parentId
	 * @param string $type type of relation (relation name)
     * @param boolean $addRelationFields set to true if you want to request on f_relation fields*
	 */
	public function addParentOfRelationCondition($childId, $type = null, $addRelationFields = false, $andOr = 'AND')
	{
		$lang = f_core_Context::getInstance()->getLang();

		if(!is_null($type))
		{
			$type = " AND `relation_type`='$type'";
		}

		$this->fieldsToGet[] = "document_id";

        $this->conditions[$andOr][] = "`child_id`=".$childId." AND `lang`='".$lang."'".$type;
        $this->relationQuery[] = "child_id";
        $this->addDistinct();
    }

	/**
	 * Add a GROUP BY to the query
	 *
	 * @param array $fieldNames
	 */
	public function addGroupBy($fieldNames)
	{
		$this->groupFields = $fieldNames;
	}

	/**
	 * Add a DISTINCT to the query
	 *
	 * @param array $fieldNames
	 */
    public function addDistinct()
    {
        $this->distinct = true;
    }

	/**
	 * Add a LIMIT to the query
	 *
	 * @param $lineNumber
	 * @param $firstLine
	 * @return unknown_type
	 */
	public function addLimit($nbResult, $offset = 0)
	{
		$this->limitNbResult = $nbResult;
		$this->limitOffset = $offset;
	}

    /**
     * Add a REGEXP condition
     * @param <type> $fieldName
     * @param <type> $dateEnd
     * @param <type> $andOr
     */
	public function addRegexp($fieldName, $regexp, $andOr = 'AND')
	{
		$counter = count($this->conditions[$andOr]);
		$this->conditions[$andOr][] = "`$fieldName` REGEXP (:regxp$counter)";
		$this->values['regxp'.$counter] = $regexp;
		$this->fieldsToGet[] = $fieldName;
	}

// ============================== Order

	/**
	 * Add an ORDER ASC in SELECT query
	 *
	 * @param string $fieldName
	 */
	public function addAscOrder($fieldName)
	{
		$this->orderConditions[] = "`$fieldName` ASC";
		$this->fieldsToGet[] = $fieldName;
	}

	/**
	 * Add an ORDER DESC in SELECT query
	 *
	 * @param string $fieldName
	 */
	public function addDescOrder($fieldName)
	{
		$this->orderConditions[] = "`$fieldName` DESC";
		$this->fieldsToGet[] = $fieldName;
	}

// ============================== Getting methods

	/**
	 * Add a column to get in SELECT query
	 *
	 * @param string $column
	 */
	public function addColumnToGet($column)
	{
		$this->columnsToGet[] = $column;

		if(!preg_match("/COUNT(.?)/", $column))
		{
			$this->fieldsToGet[] = $column;
		}
        else
        {
            $this->fieldsToGet[] = 'document_uid';
        }
	}

	/**
	 * Retrieve the SELECT query
	 *
	 * @return string SQL query
	 */
	public function getQuery($lang = null)
	{
        if($this->cleanFieldToGet())
        {
            $this->buildSelectQuery($lang);
            $this->buildWhereConditions();
            $this->buildGroupBy();
            $this->buildOrderConditions();
            $this->buildLimit();
        }

        return $this->query;
	}

	/**
	 * Return all values to get with the SELECT query
	 * format : array[PDOsqlmask] = valueToGet
	 *
	 * @return array
	 */
	public function getValues()
	{
		return $this->values;
	}

// ============================== Building methods

	/**
	 * Build the SELECT query beginning
	 * returns : SELECT fields
	 */
	private function buildSelectQuery($lang = null)
	{
		$this->query .= "SELECT ";

		if(is_null($lang)) $lang = f_core_Context::getInstance()->getLang();

		if($lang != "*")
		{
			$this->addEqualCondition("document_lang", $lang);
		}

		$this->columnsToGetString = join(", ", array_unique($this->columnsToGet));
		$this->fieldsToGetString = join(", ", array_unique($this->fieldsToGet));
        if($this->distinct == true)
        {
            $this->columnsToGetString = "DISTINCT " . $this->columnsToGetString;
        }

		$this->query .= $this->columnsToGetString;

		$this->buildFrom();
	}

	/**
	 * Add GROUP BY to query
	 */
	private function buildGroupBy()
	{
		if(count($this->groupFields)>0)
		{
			$sql = " GROUP BY ".join(", ",$this->groupFields);
			$this->query .= $sql;
		}
	}

	/**
	 * Add LIMIT to query
	 */
	private function buildLimit()
	{
		if(isset($this->limitNbResult))
		{
			if(isset($this->limitOffset) && $this->limitOffset != 0)
			{
				$this->query .= " LIMIT ".$this->limitOffset.",".$this->limitNbResult;
			}
			else
			{
				$this->query .= " LIMIT ".$this->limitNbResult;
			}
		}
	}

	/**
	 * Build the select FROM
	 * returns : FROM tableName or FROM joinedTables
	 */
	private function buildFrom()
	{
		$this->query .= " FROM ";
		$as = $this->tableName;

		if($this->isLocalized)
		{
            $as = "joinTable";
			$this->buildJoinTableColumnsToGet();

			$sql  = "((SELECT ".$this->joinTableFieldsToGetString." FROM ".$this->tableName." INNER JOIN ".$this->i18nTableName." i18n USING (`document_uid`)) UNION ";
			$sql .= "(SELECT ".$this->fieldsToGetString." FROM ".$this->tableName."))";
			$sql .= " AS joinTable";

			$this->query .= $sql;
		}
		else
		{
			$this->query .= $this->tableName;
		}

        if(!empty($this->relationQuery) && is_array($this->relationQuery))
        {
            $this->query .= " INNER JOIN f_relation ON $as.document_uid = f_relation.".join(" ON $as.document_uid = f_relation.",$this->relationQuery);
        }
	}

	/**
	 * Build all joined tables columns
	 * Make the difference in fields to get by table (i18n or not)
	 */
	private function buildJoinTableColumnsToGet()
	{
		if($this->isLocalized)
		{
			foreach(array_unique($this->fieldsToGet) as $field)
			{
				if(in_array($field, $this->nonLocalizedFields))
				{
					$conditionsFields[] = $field;
				}
				else
				{
					$conditionsFields[] = "i18n.".$field;
				}
			}

			$this->joinTableFieldsToGetString = join(", ", $conditionsFields);
		}
		else
		{
			$this->joinTableFieldsToGetString = join(", ", $this->fieldsToGet);
		}
	}

	/**
	 * Build the WHERE conditions
	 * Make the difference in fields to get by table (i18n or not)
	 */
	private function buildWhereConditions()
	{
        $this->query .= " WHERE ";

		if(count($this->conditions) >= 1)
		{
            if(isset($this->conditions['AND']) && count($this->conditions['AND']) >= 1)
            {
                $this->query .=  join(" AND ", $this->conditions['AND']);

                if(isset($this->publicationDatesCondition) && $this->publicationDatesCondition != "")
                {
                    $this->query .= " AND ".$this->publicationDatesCondition;
                }
            }
            
            if(isset($this->conditions['OR']) && count($this->conditions['OR']) >= 1)
            {
                $this->query .= ' AND (';
                $this->query .=  join(" OR ", $this->conditions['OR']);
                $this->query .= ')';
            }
		}
		else
		{
			if(isset($this->publicationDatesCondition) && $this->publicationDatesCondition != "")
			{
				$this->query .= $this->publicationDatesCondition;
			}
		}
	}

	/**
	 * Build the ORDER BY conditions
	 * Make the difference in fields to get by table (i18n or not)
	 */
	private function buildOrderConditions()
	{
		if(count($this->orderConditions) >= 1)
		{
			$this->query .= " ORDER BY ";            
            $this->query .=  join(", ", $this->orderConditions);
		}
	}

// ============================== Other methods

	/**
	 * Get non localized fields
	 *
	 * @param f_document_BaseDocument $object
	 * @return array
	 */
	private function getNonLocalizedFields($object)
	{
		$fieldsInBase = $object->nonLocalizedFields();
		$fieldsInLocal = $object->localizedFields();

		$fields = array();

		$fields['nonLocalized'] = array_diff($fieldsInBase, $fieldsInLocal);
		$fields['localizedFields'] = $fieldsInLocal;

		return $fields;
	}

	/**
	 * @return string
	 */
	public function getClassName()
	{
		return $this->className;
	}

    /**
     *  Clean the table fieldsToGet by deleting duplicated and f_relation fields.
     *
     * @return boolean
     */
    private function cleanFieldToGet()
    {
        if(!empty($this->relationQuery))
        {
            $relationFields = Array("parent_id", "child_id", "relation_type", "relation_order", "lang", "parent_model", "child_model", "status");

            $this->relationQuery = array_unique($this->relationQuery);
			$this->fieldsToGet = array_unique($this->fieldsToGet);

            foreach($relationFields as $relationField)
            {
                $key = array_search($relationField, $this->fieldsToGet);

                if($key)
                {
                    unset($this->fieldsToGet[$key]);
                }
            }
        }

        return true;
    }
}